package com.cm.tianwt.socks.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Map;

import javax.net.SocketFactory;


import com.cm.tianwt.socks.utils.Configs;
import com.cm.tianwt.socks.utils.LogUtils;
import com.cm.tianwt.socks.utils.NetworkUtils;
import com.cm.tianwt.socks.utils.Utils;

public class SocksV4Impl extends SocksHandler  {

	
	private InputStream clientInputStream;
	private OutputStream  clientOutputStream;
	
	private int command = -1;
	
	private byte[] dstPort = new byte[2];
	private byte[] dstIp = new byte[4];
	private String UID = "";
	
	
	private final int RESP_CODE_REFUSE = 1; //一般性拒绝
	private final int RESP_CODE_ALLOW = 0x5A; //运行连接
	private final int RESP_CODE_REFUSE_ON_NOT_SOPPORT_COMMAND = 0x5B; //不支持
	private final int RESP_CODE_REFUSE_ON_FAILED_IDENT = 0x5C; //鎷掔粷, 浠ｇ悊绋嬪簭鏃犳硶杩炴帴鍒癱lient鍜岀洰鏍噑erver
	private final int RESP_CODE_REFUSE_ON_NOT_MATCH_UID = 0x5D;//鎷掔粷锛宑lient鐨剈id涓嶅啀浠ｇ悊绋嬪簭鐨勬湇鍔¤寖鍥村唴(涓嶅啀鐧藉悕鍗曚腑)
	
	private  InetAddress mTargetServerAddress;
	private  int mTargetServerPort;
	
	private InetAddress mTargetClientAddress;
	private int mTargetClientPort;
	private Socket server;

	private InputStream serverInputStream;
	private OutputStream serverOutputStream;
	
	private ServerSocket mBindTempServer;
	
	
	public SocksV4Impl(Socket clientSocket, InputStream is, OutputStream os) {
		super(clientSocket);
		this.clientInputStream = is;
		this.clientOutputStream = os;
		
	}

	@Override
	public void startService() {
		SocksHandlerManager.execute(this);
	}

	@Override
	public void run() {
		
		synchronized (this) {
			try {
				doTask();
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				shutDown();
			}
		}
		
	}

	protected void shutDown() {
		Utils.closeQuiety(clientInputStream);
		Utils.closeQuiety(clientOutputStream);
		
		Utils.closeQuiety(serverInputStream);
		Utils.closeQuiety(clientOutputStream);
		
		Utils.closeQuiety(client);
		Utils.closeQuiety(server);
		Utils.closeQuiety(mBindTempServer);
	}

	private void doTask() throws IOException, Exception {
		readHeader();
		checkClient();
		switch (command) {
			case Configs.COMMAND_BIND:
					startBind();
					if(isReady()) {
						transport();
					}else {
						refuse((byte) RESP_CODE_REFUSE);
					}
				break;
			case Configs.COMMAND_CONNECT:
					startConnect();
					if(isReady()) {
					   transport();
					}else {
						refuse((byte) RESP_CODE_REFUSE); 
					}
				break;
			default:{
				refuse((byte) RESP_CODE_REFUSE); 
			}
		}
	}

	private void checkClient() throws Exception, IOException {
			checkFeatureSupport();
			checkAuthentication();
	}

	private void transport() {
		
		while( true )	{
			synchronized (this.UID.intern()) {
				try {
					boolean isEOF = sendDataToSever(); 
					//判断客户端是否还有下一次问答
					if(isEOF) {
						break;
					}
					sendDataToClient();
				} catch (Exception e) {
					e.printStackTrace();
					break;
				}
			}
			Thread.yield();
		}	
	}

	private boolean sendDataToSever() throws Exception {
		boolean isEOF = true;
		byte[] buffer = new byte[Configs.BUFFER_DEFAULT_SIZE];
		while(true) {
			int len = -1;
			try {
				if(client!=null && client.isConnected() &&!client.isInputShutdown()){
					len = clientInputStream.read(buffer, 0, buffer.length);
				 }
			}catch (SocketTimeoutException e) {
				LogUtils.D("ClientSocketTimeout ");
			}
			catch (InterruptedIOException e) {
				LogUtils.D("ClientSocketInterruptedIO");
			}
			catch(Exception e) {
				isEOF = true;
				throw e;
			}
			
			if(len==0) {
				isEOF = false;
				continue;
			}
			if(len>0) {
				logClientData(len);
				isEOF = false;
				serverOutputStream.write(buffer, 0, len);
				serverOutputStream.flush();
				continue;
			}
			break;
		}

		return isEOF;
	}

	private void sendDataToClient() throws Exception {
		byte[] buffer = new byte[Configs.BUFFER_DEFAULT_SIZE];
		while(true) {
			int len = -1;
			try {
				if(server!=null && server.isConnected() && !server.isInputShutdown()){
					len = serverInputStream.read(buffer, 0, Configs.BUFFER_DEFAULT_SIZE);
				}
			}catch(SocketTimeoutException e) {
				LogUtils.E("ServerSocketTimeout");
			}catch (InterruptedIOException e) {
				LogUtils.E("ServerSocketInterrupted");
			}catch (Exception e) {
				throw e;
			}
			
			if(len==0) continue;
			if(len>0) {
				logServerData(len);
				clientOutputStream.write(buffer, 0, len);
				clientOutputStream.flush();
				continue;
			}
			break;
		}
	}


	private void checkFeatureSupport() throws Exception {
		if( (command < Configs.COMMAND_CONNECT) || (command > Configs.COMMAND_BIND) )	{
			refuse( (byte)RESP_CODE_REFUSE_ON_NOT_SOPPORT_COMMAND );
			throw	new Exception( "Socks 4 - Unsupported Command : "+command );
		}
		if( !parseAddress() )	{  
			refuse( (byte)RESP_CODE_REFUSE_ON_FAILED_IDENT );	
			throw new Exception( "Socks 4 - Unknown Host/IP address '"+mTargetServerAddress.toString() );
		}
		
	}

	private void checkAuthentication() throws IOException, Exception {
		if(!Configs.USE_WHITE_LIST) {
			return;
		}
		if(!Configs.checkWhiteList(this.UID,this.mTargetServerAddress.getHostName())) {
			refuse( (byte)RESP_CODE_REFUSE_ON_NOT_MATCH_UID );	
			throw new IOException( "Socks 4 - Unknown Host/IP address '"+mTargetServerAddress.toString() );
		}
	}
	public	boolean	parseAddress()	{
		
		mTargetServerAddress		= Utils.calcInetIpV4Address(dstIp );
		mTargetServerPort			= Utils.calcPort( dstPort[0], dstPort[1] );
		
		mTargetClientAddress		= this.client.getInetAddress();
		mTargetClientPort			= this.client.getPort();
		
		return ( (mTargetServerAddress != null) && (mTargetServerPort >= 0) );
	}	
	
	private void refuse(byte respCode) throws IOException {
		response(respCode);
	}
	private void accept() throws IOException {
		response((byte)RESP_CODE_ALLOW);
	}

	private void response(byte respCode) throws IOException {
		
		LogUtils.D("SocksV4",Utils.getCodeDesc(respCode));
		
		byte[] responeData = new byte[8];
		responeData[0]= 0;  //响应码是0x00而不是0x04
		responeData[1]= respCode;
		responeData[2]= dstPort[0];
		responeData[3]= dstPort[1];
		
		responeData[4]= dstIp[0];
		responeData[5]= dstIp[1];
		responeData[6]= dstIp[2];
		responeData[7]= dstIp[3];
		if(client!=null && client.isConnected()){
			sendToClient(responeData);
		}
	}

	private void sendToClient(byte[] responeData) throws IOException {
		clientOutputStream.write(responeData, 0, responeData.length);
		clientOutputStream.flush();
	}

	private void startConnect() throws Exception {
		
		try {
			SocketFactory socketFactory = SocketFactory.getDefault();
			Socket targetServerSocket = socketFactory.createSocket(mTargetServerAddress, mTargetServerPort);
			targetServerSocket.setTcpNoDelay(true);
			targetServerSocket.setSoTimeout(Configs.SO_TIMEOUT);
			
			server = targetServerSocket;
			
			serverInputStream 		= targetServerSocket.getInputStream();
			serverOutputStream  	= targetServerSocket.getOutputStream();
			
			accept();
		} catch (Exception e) {
			e.printStackTrace();
			refuse( (byte) RESP_CODE_REFUSE_ON_NOT_SOPPORT_COMMAND); // Connection Refused
			throw new Exception("Socks 4 - Can't connect to " +Utils.getSocketInfo( server ) );
		}
		
	}
	
	public boolean isReady() {
		return (server!=null && server.isConnected()&&client!=null&& client.isConnected());
	}

	private void startBind() throws IOException {
		InetAddress		hostAddress		= null;
		int				hostPort	= 0; //鍏变韩绔彛
		
		Map<String, Object> localInetIdent = NetworkUtils.getLocalInetIdent();
		if(localInetIdent==null) {
			refuse( (byte) RESP_CODE_REFUSE_ON_NOT_SOPPORT_COMMAND);
			return;
		}
		String ipHost  = (String) localInetIdent.get("ip");
		hostAddress = InetAddress.getByName(ipHost);
		
		LogUtils.E("鏈満IP =" +hostAddress);
		
		try	{	
			mBindTempServer = new ServerSocket( 0 );
			//创建临时ServerSocket，然后通知client,client通过Connect方式通知目标主机连接此临时ServerSocket
			//端口设置为0，则表示临时ServerSocket的端口由系统决定，然后可以通过getLocalPort方式获取
			mBindTempServer.setSoTimeout( Configs.READ_TIMEOUT );
			hostPort	= mBindTempServer.getLocalPort();
		}
		catch( IOException e )	{ 
			LogUtils.E( "Error in BIND() - Can't BIND at any Port" );
			bindReply( (byte)RESP_CODE_REFUSE_ON_FAILED_IDENT, hostAddress,hostPort );
			throw e;
		}

		LogUtils.E( "BIND at : <"+ipHost+":"+hostPort+">" );
		bindReply( (byte)RESP_CODE_ALLOW, hostAddress, hostPort ); 
		//响应socks client,socks client通过connect让服务器连接到该socks
									
		if( clientIsRefusedBind() ) {  
			//判断客户端是否发来数据，如果发来数据，则说明客户端拒绝
			LogUtils.E( "BIND - Client connection closed" );
			shutDown();
			return;
		}
		
		Socket	socket = null;
		try {
			socket = mBindTempServer.accept();
			socket.setSoTimeout( Configs.SO_TIMEOUT );
		}
		catch( IOException e ) {
			socket.close();
			throw e;
		}
		
		mTargetServerAddress	= socket.getInetAddress();
		mTargetServerPort	    = socket.getPort();
		
		bindReply( (byte)90,	socket.getInetAddress(),socket.getPort() );
		
		server =  socket;
		
		serverInputStream 		= server.getInputStream();
		serverOutputStream  	= server.getOutputStream();
		
		mBindTempServer.close();
	}
	
	private boolean clientIsRefusedBind() {
		try {
			if(clientInputStream==null) return true;
			int len = clientInputStream.read(new byte[128], 0, 128);
			return len>=0;
		} catch( InterruptedIOException e )		{
			return	true;
		}catch (IOException e) {
			e.printStackTrace();
		}
		return false;
	}

	protected void bindReply(byte code, InetAddress hostAddress, int hostPort) throws IOException {
		
		byte	IP[] = {0,0,0,0};
		
		LogUtils.I( "Reply to Client : \""+Utils.getCodeDesc( code )+"\"" );
		
		byte[] REPLY = new byte[8];
		if( hostAddress != null )	IP = hostAddress.getAddress();
						
		REPLY[0]= 0;
		REPLY[1]= code;
		REPLY[2]= (byte)((hostPort & 0xFF00) >> 8);
		REPLY[3]= (byte) (hostPort & 0x00FF);
		REPLY[4]= IP[0];
		REPLY[5]= IP[1];
		REPLY[6]= IP[2];
		REPLY[7]= IP[3];
			
		if(client!=null && client.isConnected()){
			sendToClient(REPLY);
		}
		else	{
			LogUtils.I( "Closed BIND Client Connection" );
		}
		
	}

	private void initIOStream() throws IOException {

		clientInputStream = client.getInputStream();
		clientOutputStream = client.getOutputStream();
	}

	private void readHeader() throws IOException {
		
		initIOStream();
		
		command = clientInputStream.read();
		clientInputStream.read(dstPort, 0, 2);
		clientInputStream.read(dstIp, 0, 4);
		StringBuffer sBuffer = new StringBuffer();
		
		int byteNum = clientInputStream.read();
		while (byteNum!=0x00) {
			sBuffer.append((char)byteNum);
			byteNum = clientInputStream.read();
		}
		sBuffer.append(0x00);
		this.UID = sBuffer.toString();
		
		LogUtils.E("Socks4", UID);
		
	}

	public	void	logServerData( int traffic )	{
		LogUtils.I("SERVER : "+
					Utils.getSocketInfo( server ) +
					" << <"+
					mTargetServerAddress.getHostName()+"/"+
					mTargetServerAddress.getHostAddress()+":"+
					mTargetServerPort+"> : " + 
					traffic +" bytes." );
	}
	

	public	void	logClientData( int traffic )	{
		LogUtils.I("Client: "+
					Utils.getSocketInfo( client ) +
					" >> <"+
					mTargetClientAddress.getHostName()+"/"+
					mTargetClientAddress.getHostAddress()+":"+
					mTargetClientPort+"> : " + 
					traffic +" bytes." );
	}
	
}
